在一场10人的英雄联盟的比赛中,每一支队伍各有5名选手参与,每一名选手在队伍中有各自的团队定位:上单(TOP)、打野(JUNGLE)、中路(MID)、下路(BOTCARRY)和辅助(SUPPORT)。在这个EDA部分,我会用数据可视化的方式来详细分析2018年NALCS赛区春季赛数据,通过对团队定位的各项平均数据的分析,希望带大家从数据的角度了解一场比赛中的各个位置。
首先,我们根据团队定位来看一下场均击杀(Kills Per Game)的分布。
nalcs_plot_player_avgs +
geom_histogram(mapping = aes(x = kills, y = ..density.., fill = teamRole),
color = "black", size = 0.5, alpha = .6, binwidth = .125) +
labs(
title = "Kills Per Game Histogram, NALCS 2018 Spring Split",
subtitle = "Distribution of Kills")nalcs_plot_player_avgs +
geom_density(mapping = aes(x = kills, color = teamRole, fill = teamRole),
alpha = .2, size = 0.5) +
labs(
title = "Kills Per Game Density Plot, NALCS 2018 Spring Split",
subtitle = "Distribution of Kills Across Team Roles")nalcs_plot_player_avgs +
geom_boxplot(mapping = aes(x = teamRole, y = kills, fill = teamRole),
size = 0.5, alpha = .6) +
geom_jitter(width = 0.15, mapping = aes(x = teamRole, y = kills, color = teamRole)) +
labs(
title = "Kills Per Game Box Plot, NALCS 2018 Spring Split",
subtitle = "Distribution of Kills Across Team Roles")从以上场均击杀的柱状图、密度图和箱线图中我们可以清楚的发现:在2018年的NALCS春季赛中,团队定位为辅助的选手拥有最低的场均击杀,其次是打野和上单;团队定位为中单和下路的选手拥有最高的场均击杀。这其实符合我们的预期,辅助选手在比赛中的主要定位是保护、控制和提供视野等,而中单和下路(玩家口中的双C)选手的定位是输出,自然容易拥有更高的场均击杀。
接下来让我们根据团队定位看一看场均死亡(Deaths Per Game)的分布。
nalcs_plot_player_avgs +
geom_histogram(mapping = aes(x = deaths, y = ..density.., fill = teamRole),
color = "black", size = 0.5, alpha = .6, binwidth = .125) +
labs(
title = "Deaths Per Game Histogram, NALCS 2018 Spring Split",
subtitle = "Distribution of Deaths")nalcs_plot_player_avgs +
geom_density(mapping = aes(x = deaths, color = teamRole, fill = teamRole),
alpha = .2, size = 0.5) +
labs(
title = "Deaths Per Game Density Plot, NALCS 2018 Spring Split",
subtitle = "Distribution of Deaths Across Team Roles")nalcs_plot_player_avgs +
geom_boxplot(mapping = aes(x = teamRole, y = deaths, fill = teamRole),
size = 0.5, alpha = .6) +
geom_jitter(width = 0.15, mapping = aes(x = teamRole, y = deaths, color = teamRole)) +
labs(
title = "Deaths Per Game Box Plot, NALCS 2018 Spring Split",
subtitle = "Distribution of Assists Across Team Roles")从数据的角度我们可以发现:相较于场均击杀,选手的场均死亡数会更相近一些;但是辅助选手和上单选手通常来说会死得更多一些。
从游戏本身的角度来说(也是我个人的理解):在一场比赛中,上路这条线很长,所以当兵线处于推进的过程非常容易遭遇敌方打野的gank,同时上路也是目前职业比赛在前期非常喜欢针对的一条路,所以会导致普遍来说上路选手的场均死亡会较高。而辅助选手场均死亡数高的原因有许多,一是辅助位吃团队最少的经济,等级低且装备差,通常是较为好击杀的一个位置;二是辅助位需要承担团队中做视野的责任,在做视野的过程中通常容易被针对击杀;三是辅助位通常起团队中开团或是保护C位的作用,献身冲阵打出控制和替C位挡技能都是导致死亡的重要原因。
因此,辅助选手和上单选手拥有较高的场均死亡数这个结论其实并不令人惊讶。
接下来,我们再来看一下场均助攻(Assists Per Game)数据的分布。
首先来解释一下什么是“助攻”:助攻可以理解为“帮助队友击杀”,无论是在一次击杀中贡献了一部分伤害,或是提供了控制还是治疗、护盾等buff,在那次击杀结束后都会增加一次助攻数。因此场均助攻我们可以理解为一名选手平均在一场游戏中辅助队友造成了多少次击杀,从另一个方面可以理解为选手参与战斗的积极程度(除开击杀本身)。
nalcs_plot_player_avgs +
geom_histogram(mapping = aes(x = assists, y = ..density.., fill = teamRole),
color = "black", size = 0.5, alpha = .6, binwidth = .2) +
labs(
title = "Assists Per Game Histogram, NALCS 2018 Spring Split",
subtitle = "Distribution of Assists")nalcs_plot_player_avgs +
geom_density(mapping = aes(x = assists, color = teamRole, fill = teamRole),
alpha = .2, size = 0.5) +
labs(
title = "Assists Per Game Density Plot, NALCS 2018 Spring Split",
subtitle = "Distribution of Assists Across Team Roles")nalcs_plot_player_avgs +
geom_boxplot(mapping = aes(x = teamRole, y = assists, fill = teamRole),
size = 0.5, alpha = .6) +
geom_jitter(width = 0.15, mapping = aes(x = teamRole, y = assists, color = teamRole)) +
labs(
title = "Assists Per Game Box Plot, NALCS 2018 Spring Split",
subtitle = "Distribution of Assists Across Team Roles")从上述图表中我们可以得出和场均击杀几乎相反的结论:虽然辅助和打野选手并不擅长造成击杀,但是辅助和打野选手在场均助攻这项数据上拥有明显的领先。
从游戏本身的理解来说:辅助和打野通常是大小团战的发起者,而且辅助英雄大多数拥有控制、治疗或护盾等技能,自然更容易增加自己的场均助攻数。而场均击杀数这项数据通常会由造成最多伤害的中单和下路获得。
除了场均击杀、场均死亡、场均助攻这三项数据外,每个团队位置在比赛中还有更多的数据,以下我将分别展示NALCS2018春季赛选手场均数据的柱状图、密度图和箱线图。
# Histograms
nalcs_plot_player_avgs_gathered +
geom_histogram(mapping = aes(x = valuePerGame, y = ..density.., fill = teamRole),
color = "black", alpha = .6) +
facet_wrap(~varName, scales = "free", ncol = 2) +
labs(
title = "Player Averages Per Game Histograms, NALCS 2018 Spring Split",
subtitle = "Distribution of Values")nalcs_plot_player_avgs_gathered +
geom_density(mapping = aes(x = valuePerGame, color = teamRole, fill = teamRole),
alpha = .3, size = 0.4) +
facet_wrap(~varName, scales = "free", ncol = 2) +
labs(
title = "Player Averages Per Game Density Plots, NALCS 2018 Spring Split",
subtitle = "Distribution of Values")# Box Plots
nalcs_plot_player_avgs_gathered +
geom_boxplot(mapping = aes(x = teamRole, y = valuePerGame, fill = teamRole),
size = 0.5, alpha = .6) +
geom_jitter(width = 0.15, mapping = aes(x = teamRole, y = valuePerGame, color = teamRole)) +
facet_wrap(~ varName, scales = "free", ncol = 5) +
theme(axis.text.x = element_text(angle=30, vjust=0.6)) +
labs(
title = "Player Averages per Game Box Plots, NALCS 2018 Spring Split",
subtitle = "Distribution Across Team Roles")从以上的图表中我们可以发现每个位置的一些特点:
上单: 最少助攻, 最少对他人治疗,最多控制时间
打野: 最多治疗, 最多承伤和减伤, 最高视野得分, 最多排眼, 击杀最多中立生物
中单: 造成最多魔法伤害, 最高等级
下路: 造成最多伤害, 造成最多物理伤害, 最少治疗, 最少承伤
辅助: 最少击杀, 造成最少伤害, 最多助攻, 最多对单位治疗, 购买最多眼, 最低等级
因此,总的来说,在S8的NALCS的春季赛中,上单位通常作为首要承伤位和控制提供者,这在赛场中的体现即为选择科加斯、塞恩、奥恩这类可以提供团体控制的大坦克;打野位通常作为第二的承伤和控图位置,这在赛场中的体现便是选择特朗德尔、雷克赛、斯卡纳这类(半)坦克打野,打法也是偏控图为主;中单和下路双C在绝大多数版本的定位都是输出位,而中单更多提供团队的魔法输出(AP),而下路基本都是提供物理输出(AD)的射手;辅助位通常则是提供团队的控制和对队友的保护,因此在S8的赛场上通常是锤石、布隆、洛、莫甘娜这类选择。
看完了数据,我们来看一下赛场上真实的情况吧:
下图是2018NALCS春季赛的决赛,决赛的两只队伍是TL和C9,这两只队伍都是北美赛区的老牌强队,在第三场(也是决赛的最后一场)的比赛中双方BP的阵容如下:
在这场比赛中,TL和C9都选用了坦克上路,打野一边是偏向输出和控制的酒桶,一边是偏向控图和承伤的特朗德尔,中路都是AP输出大核,下路都是版本AD射手,辅助一边是偏向进攻和控制的锤石,一边是偏向保护和承伤的布隆。这样的BP选择是基本符合我们之前的分析的(也就是顺从当时版本的选择)。
最高输出奖
nalcs_matches_player_avgs %>%
select(summonerName,teamRole, totalDamageDealtToChampions) %>%
arrange(desc(totalDamageDealtToChampions)) %>%
head(5) %>%
kable()| summonerName | teamRole | totalDamageDealtToChampions |
|---|---|---|
| 100 Cody Sun | BOTCARRY | 28836.78 |
| CLG Stixxay | BOTCARRY | 28736.17 |
| C9 Sneaky | BOTCARRY | 25419.39 |
| CLG huhi | MID | 24932.50 |
| FLY WILDTURTLE | BOTCARRY | 24360.11 |
在整个s8NALCS春季赛中,100T的下路选手Cody Sun(孙哥)平均每场比赛能打超过28800点伤害,不愧为最高输出奖。同时我们发现在5个最高伤害的选手中,有4名都是下路。
最能抗伤奖
nalcs_matches_player_avgs %>%
select(summonerName,teamRole, totalDamageTaken) %>%
arrange(desc(totalDamageTaken)) %>%
head(5) %>%
kable()| summonerName | teamRole | totalDamageTaken |
|---|---|---|
| CLG Reignover | JUNGLE | 39212.00 |
| FLY AnDa | JUNGLE | 38665.50 |
| C9 Svenskeren | JUNGLE | 36264.91 |
| FOX Dardoch | JUNGLE | 33999.27 |
| 100 Meteos | JUNGLE | 33493.37 |
在整个S8NALCS春季赛中,最能抗伤害的非CLG打野选手Reignover莫属,他平均每场能抗超过39000点伤害。令人惊讶的是,和上单比起来,这个赛季似乎打野位才是真正的(最能)抗伤的位置。
北美法王奖
nalcs_matches_player_avgs %>%
select(summonerName,teamRole, magicDamageDealtToChampions) %>%
arrange(desc(magicDamageDealtToChampions)) %>%
head(5) %>%
kable()| summonerName | teamRole | magicDamageDealtToChampions |
|---|---|---|
| TSM Bjergsen | MID | 22514.54 |
| OPT PowerOfEvil | MID | 21771.11 |
| CLG huhi | MID | 20964.00 |
| FOX Fenix | MID | 20623.74 |
| FLY Keane | MID | 19671.33 |
在整个S8NALCS春季赛中,TSM的中单Bjergsen选手以场均22514点AP输出荣获北美法王奖,不愧是带着4颗真眼打比赛的院长,致敬。
在这个部分,我将使用聚类算法中的KMeans和分类算法KNN来从侧面验证我们从数据可视化中得到的线索:不同位置的选手在比赛中的各项数据是带有某些“特征”的,而且我们也许可以在不知道选手位置的情况下根据这些“特征”来对选手的位置进行预测分类。
在这个部分我选取了2018NALCS春季赛各选手最具代表性的17个特征,它们分别为:
| variableName | description |
|---|---|
| kills | 击杀数,即选手在一场比赛中造成的击杀数量 |
| assists | 助攻数,即选手在一场比赛中造成的助攻数量 |
| magicDamageDealt | 造成的魔法伤害,即选手在一场比赛中造成的累计魔法伤害,包括对敌方英雄、防御塔和中立生物等 |
| physicalDamageDealt | 造成的物理伤害,即选手在一场比赛中造成的累计物理伤害,包括对敌方英雄、防御塔和中立生物等 |
| magicDamageDealtToChampions | 对英雄造成的魔法伤害,即选手在一场比赛中对敌方英雄造成的累计魔法伤害 |
| physicalDamageDealtToChampions | 对英雄造成的物理伤害,即选手在一场比赛中对敌方英雄造成的累计物理伤害 |
| totalHeal | 治疗总量,即选手在一场比赛中获得的治疗总量 |
| totalUnitsHealed | 单位治疗总量,即选手在一场比赛中对友方英雄、自己、友方建筑、小兵的治疗数量 |
| damageSelfMitigated | 减伤,即选手在一场比赛中通过护盾、护甲、魔抗等方式减少的伤害量 |
| totalDamageTaken | 总承伤,即选手在一场比赛中承受来源于敌方英雄、防御塔、小兵、中立生物等的总伤害 |
| neutralMinionsKilled | 中立生物击杀数,即选手在一场比赛在击杀的中立生物的总数量 |
| timeCCingOthers | 控制时间,即除去硬控外的控制时间(如软控制和减速) |
| totalTimeCrowdControlDealt | 总控制时间,即选手在一场比赛中提供的硬控制时间总和 |
| champLevel | 英雄等级,即选手在比赛结束时的英雄等级 |
| visionWardsBoughtInGame | 真视守卫购买数,即选手在一场比赛中购买的真眼的数量 |
| wardsPlaced | 视野数量,即选手在一场比赛中放下的眼的数量(包括真眼和假眼) |
| wardsKilled | 排眼数量,即选手在一场比赛中排除的眼的数量(包括真眼和假眼) |
首先我们先来加载s8春季赛各赛区选手的平均数据,之后再合并成大的数据集,在这个数据集中,我们可以找到各赛区的各位选手在整个春季赛的平均数据。注:在这里我们只关注上场超过6次的选手。
# Import averages data for all available leagues
nalcs_season_summoner_avgs <- read.csv("./datasets/nalcs/nalcs_spring2018_season_summoner_avgs.csv") %>%
select(-X)
eulcs_season_summoner_avgs <- read.csv("./datasets/eulcs/eulcs_spring2018_season_summoner_avgs.csv") %>%
select(-X)
lck_season_summoner_avgs <- read.csv("./datasets/lck/lck_spring2018_season_summoner_avgs.csv") %>%
select(-X)
lms_season_summoner_avgs <- read.csv("./datasets/lms/lms_spring2018_season_summoner_avgs.csv") %>%
select(-X)
# Putting all leagues together
all_leagues_summoner_avgs <-
eulcs_season_summoner_avgs %>%
bind_rows(lms_season_summoner_avgs) %>%
bind_rows(lck_season_summoner_avgs) %>%
bind_rows(nalcs_season_summoner_avgs) %>%
# Removing players who haven't played at least six games.
filter(wins + losses >= 6)
str(all_leagues_summoner_avgs %>% select(1:10))
remove(nalcs_season_summoner_avgs, eulcs_season_summoner_avgs, lck_season_summoner_avgs, lms_season_summoner_avgs)在训练模型之前,预处理数据是非常重要的一步。首先,我们先选择之前我们提到过的17个特征:
data_selected <- all_leagues_summoner_avgs %>%
select(kills, assists, magicDamageDealt, physicalDamageDealt,
magicDamageDealtToChampions, physicalDamageDealtToChampions,
totalHeal, totalUnitsHealed, damageSelfMitigated, totalDamageTaken,
neutralMinionsKilled, timeCCingOthers, totalTimeCrowdControlDealt,
champLevel, visionWardsBoughtInGame, wardsPlaced, wardsKilled)再来检查一下数据中是否含有缺失值:
## [1] FALSE
由于我们选择的17个特征都是数值型变量,我们可以先看看它们之间的关系矩阵(correlation matrix): 两个变量间的相关系数的绝对值越大说明两个变量间的线性关系越强,说明两者可能包含了较多重复的信息,为了简化模型同时防止过拟合,我们只需要在包含大量重复信息的变量中选择其一即可。在这里我会特别关注相关系数大于0.85的变量。
因此,我们在“magicDamageDealt”和“magicDamageDealtToChampions”中选择“magicDamageDealtToChampions”;
在“physicalDamageDealt”和“physicalDamageDealtToChampions”中选择“physicalDamageDealtToChampions”;
在“totalHeal”和“totalDamageTaken”中选择“totalDamageTaken”;
在“damageSelfMitigated”和“totalDamageTaken”中选择“totalDamageTaken”;
在“visionWardsBoughtInGame”和“wardsPlaced”中选择“wardsPlaced”。
为了保证各个特征的尺度一致,在训练KMeans模型之前我们需要标准化各个特征,这里我们使用Z-Score Standardization来标准化数据。
set.seed(1234)
k <- kmeans(data_scaled, centers = 5, nstart = 25)
fviz_cluster(k, data = data_scaled)| cluster | teamRole | n |
|---|---|---|
| 1 | SUPPORT | 41 |
| 2 | MID | 1 |
| 2 | TOP | 46 |
| 3 | JUNGLE | 53 |
| 4 | BOTCARRY | 45 |
| 5 | MID | 47 |
在所有的聚类结果中,KMeans算法只把EEW战队的中单SSUN选手归到上单这个聚类中,其余所有选手都被分到了与其位置对应的聚类中了,可以说KMeans算法从侧面证明了5个位置的选手在比赛的数据上有所区别,并且每个位置有其独特的特点。
result %>%
select(summonerName, teamRole, cluster) %>%
filter(cluster == 2) %>%
filter(teamRole == "MID") %>%
kable()| summonerName | teamRole | cluster |
|---|---|---|
| EEW SSUN | MID | 2 |
既然KMeans算法已经从侧面说明了5个位置的选手在比赛中的分别有其独特的数据特点,那么我们是否可以构造一个机器学习的分类模型,使得我们输入某一位选手在某一场比赛中的各项数据,让分类器根据各个位置的数据特点来对该名选手的位置(团队定位)进行分类?
首先我们先来加载s8春季赛各赛区每一场比赛的数据,之后再合并成大的数据集,在这个数据集中,我们可以找到某一选手在某一场比赛中的各项详细数据。
nalcs_season_match_player_stats <- read.csv("./datasets/nalcs/nalcs_spring2018_match_player_stats.csv") %>% select(-X)
eulcs_season_match_player_stats <- read.csv("./datasets/eulcs/eulcs_spring2018_match_player_stats.csv") %>% select(-X)
lck_season_match_player_stats <- read.csv("./datasets/lck/lck_spring2018_match_player_stats.csv") %>% select(-X)
lms_season_match_player_stats <- read.csv("./datasets/lms/lms_spring2018_match_player_stats.csv") %>% select(-X)
msi_season_match_player_stats <- read.csv("./datasets/msi/msi_2018_match_player_stats.csv") %>% select(-X)
all_leagues_match_player_stats <-
nalcs_season_match_player_stats %>%
bind_rows(eulcs_season_match_player_stats) %>%
bind_rows(lck_season_match_player_stats) %>%
bind_rows(lms_season_match_player_stats) %>%
bind_rows(msi_season_match_player_stats) %>%
mutate(roleLane = paste(role, lane, sep = ", "))
str(all_leagues_match_player_stats %>% select(1:10))
remove(nalcs_season_match_player_stats, eulcs_season_match_player_stats, lck_season_match_player_stats, lms_season_match_player_stats, msi_season_match_player_stats)## [1] 6960 146
由于之前KMeans的聚类效果很不错,这里我打算直接使用之前的特征。
为了保证各个特征的尺度一致,在使用KNN模型之前我们需要标准化各个特征,这里我们使用Min-Max Normalization来标准化数据。
我们将数据集中的70%用于训练,剩余的30%用于测试。
# Split dataset into training and testing
library(caret)
set.seed(1234)
train_index <- caret::createDataPartition(all_leagues_match_player_stats$teamRole, p = 0.7, list = FALSE, times = 1)
train_data <- player_scaled[train_index,]
test_data <- player_scaled[-train_index,]
train_label <- all_leagues_match_player_stats[train_index,]$teamRole
test_label <- all_leagues_match_player_stats[-train_index,]$teamRole## [1] 4875 12
由于我们训练集中有4875个数据,根据经验法,我们先尝试设置K = \(\sqrt{4875} \approx 70\)
library(class)
knn <- knn(train=train_data, test=test_data, cl=train_label, k=70)
knn.10 <- knn(train=train_data, test=test_data, cl=train_label, k=10)| BOTCARRY | JUNGLE | MID | SUPPORT | TOP | |
|---|---|---|---|---|---|
| BOTCARRY | 385 | 1 | 2 | 0 | 21 |
| JUNGLE | 0 | 352 | 0 | 23 | 58 |
| MID | 4 | 1 | 363 | 22 | 21 |
| SUPPORT | 4 | 28 | 29 | 370 | 38 |
| TOP | 24 | 35 | 23 | 2 | 279 |
## Confusion Matrix and Statistics
##
## Reference
## Prediction BOTCARRY JUNGLE MID SUPPORT TOP
## BOTCARRY 385 1 2 0 21
## JUNGLE 0 352 0 23 58
## MID 4 1 363 22 21
## SUPPORT 4 28 29 370 38
## TOP 24 35 23 2 279
##
## Overall Statistics
##
## Accuracy : 0.8388
## 95% CI : (0.8224, 0.8544)
## No Information Rate : 0.2
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.7986
##
## Mcnemar's Test P-Value : 1.166e-06
##
## Statistics by Class:
##
## Class: BOTCARRY Class: JUNGLE Class: MID Class: SUPPORT
## Sensitivity 0.9233 0.8441 0.8705 0.8873
## Specificity 0.9856 0.9514 0.9712 0.9406
## Pos Pred Value 0.9413 0.8129 0.8832 0.7889
## Neg Pred Value 0.9809 0.9607 0.9677 0.9709
## Prevalence 0.2000 0.2000 0.2000 0.2000
## Detection Rate 0.1847 0.1688 0.1741 0.1775
## Detection Prevalence 0.1962 0.2077 0.1971 0.2249
## Balanced Accuracy 0.9544 0.8978 0.9209 0.9140
## Class: TOP
## Sensitivity 0.6691
## Specificity 0.9496
## Pos Pred Value 0.7686
## Neg Pred Value 0.9199
## Prevalence 0.2000
## Detection Rate 0.1338
## Detection Prevalence 0.1741
## Balanced Accuracy 0.8094
K = 10时准确率最高达到85.94724%。
## Confusion Matrix and Statistics
##
## Reference
## Prediction BOTCARRY JUNGLE MID SUPPORT TOP
## BOTCARRY 386 1 1 0 23
## JUNGLE 0 358 1 18 51
## MID 3 1 360 21 16
## SUPPORT 1 21 29 372 19
## TOP 27 36 26 6 308
##
## Overall Statistics
##
## Accuracy : 0.8556
## 95% CI : (0.8398, 0.8705)
## No Information Rate : 0.2
## P-Value [Acc > NIR] : < 2e-16
##
## Kappa : 0.8195
##
## Mcnemar's Test P-Value : 0.08474
##
## Statistics by Class:
##
## Class: BOTCARRY Class: JUNGLE Class: MID Class: SUPPORT
## Sensitivity 0.9257 0.8585 0.8633 0.8921
## Specificity 0.9850 0.9580 0.9754 0.9580
## Pos Pred Value 0.9392 0.8364 0.8978 0.8416
## Neg Pred Value 0.9815 0.9644 0.9662 0.9726
## Prevalence 0.2000 0.2000 0.2000 0.2000
## Detection Rate 0.1851 0.1717 0.1727 0.1784
## Detection Prevalence 0.1971 0.2053 0.1923 0.2120
## Balanced Accuracy 0.9553 0.9083 0.9194 0.9251
## Class: TOP
## Sensitivity 0.7386
## Specificity 0.9430
## Pos Pred Value 0.7643
## Neg Pred Value 0.9352
## Prevalence 0.2000
## Detection Rate 0.1477
## Detection Prevalence 0.1933
## Balanced Accuracy 0.8408
在上述表格当中,值得注意的是:对TOP(上路)这个位置的预测相对于其他位置有明显更低的Sensitivity。查阅Caret包的document我们可以知道这里的Sensitivity即Recall(查全率),指的是被正确检索的样本数与应当被检索的样本总数之比。也就是说,相比于其他位置,上单选手的数据常常被错误地分类到了其他位置。
根据之前的混淆矩阵,我们发现上单选手有较多部分被分类为辅助和打野,小部分被分类为中单和下路,这引起了我们的思考:我们是否可以加入一些其他的特征来更加细化地区分上单和其他位置?
在这里我们尝试构建一个新的特征和加入三个新特征:
| newVarName | newDescription |
|---|---|
| physDmgToChampsToDmgTakenRatio | 对英雄物理输出 / 总承伤,用以区分上单和下路/中单 |
| totalMinionsKilled | 敌方小兵击杀数量,用以区分上单和打野/辅助 |
| damageTakenPerMinDeltas.0.10 | 前十分钟每分钟承伤,用以区分上单和打野 |
| creepsPerMinDeltas.0.10 | 前十分钟每分钟击杀敌方小兵数量,用于区分上单和辅助/打野 |
all_leagues_match_player_stats <- all_leagues_match_player_stats %>%
mutate("physDmgToChampsToDmgTakenRatio" = physicalDamageDealtToChampions / totalDamageTaken)
player_selected <- all_leagues_match_player_stats %>%
select(kills, assists, magicDamageDealtToChampions, physicalDamageDealtToChampions,
totalUnitsHealed, totalDamageTaken, neutralMinionsKilled, timeCCingOthers,
totalTimeCrowdControlDealt, champLevel, wardsPlaced, wardsKilled, totalMinionsKilled, damageTakenPerMinDeltas.0.10,
physDmgToChampsToDmgTakenRatio, creepsPerMinDeltas.0.10)
player_scaled <- normalize(player_selected)
set.seed(1234)
train_index <- caret::createDataPartition(all_leagues_match_player_stats$teamRole, p = 0.7, list = FALSE, times = 1)
train_data <- player_scaled[train_index,]
test_data <- player_scaled[-train_index,]
train_label <- all_leagues_match_player_stats[train_index,]$teamRole
test_label <- all_leagues_match_player_stats[-train_index,]$teamRole| BOTCARRY | JUNGLE | MID | SUPPORT | TOP | |
|---|---|---|---|---|---|
| BOTCARRY | 389 | 1 | 2 | 0 | 18 |
| JUNGLE | 0 | 356 | 0 | 18 | 47 |
| MID | 3 | 1 | 359 | 20 | 17 |
| SUPPORT | 1 | 21 | 29 | 374 | 21 |
| TOP | 24 | 38 | 27 | 5 | 314 |
## Confusion Matrix and Statistics
##
## Reference
## Prediction BOTCARRY JUNGLE MID SUPPORT TOP
## BOTCARRY 389 1 2 0 18
## JUNGLE 0 356 0 18 47
## MID 3 1 359 20 17
## SUPPORT 1 21 29 374 21
## TOP 24 38 27 5 314
##
## Overall Statistics
##
## Accuracy : 0.8595
## 95% CI : (0.8438, 0.8741)
## No Information Rate : 0.2
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.8243
##
## Mcnemar's Test P-Value : 0.0401
##
## Statistics by Class:
##
## Class: BOTCARRY Class: JUNGLE Class: MID Class: SUPPORT
## Sensitivity 0.9329 0.8537 0.8609 0.8969
## Specificity 0.9874 0.9610 0.9754 0.9568
## Pos Pred Value 0.9488 0.8456 0.8975 0.8386
## Neg Pred Value 0.9833 0.9633 0.9656 0.9738
## Prevalence 0.2000 0.2000 0.2000 0.2000
## Detection Rate 0.1866 0.1707 0.1722 0.1794
## Detection Prevalence 0.1966 0.2019 0.1918 0.2139
## Balanced Accuracy 0.9601 0.9074 0.9182 0.9269
## Class: TOP
## Sensitivity 0.7530
## Specificity 0.9436
## Pos Pred Value 0.7696
## Neg Pred Value 0.9386
## Prevalence 0.2000
## Detection Rate 0.1506
## Detection Prevalence 0.1957
## Balanced Accuracy 0.8483
通过观察新的混淆矩阵我们发现,加入了新的特征之后,其他位置的数据没有明显变化,但上单的查全率有略微的提升(2%),同时模型的准确率也有略微的提升。
接下来让我们看一下究竟是哪些上单英雄被错误地分类成了其他位置吧:
| BOTCARRY | JUNGLE | MID | SUPPORT | TOP | |
|---|---|---|---|---|---|
| Camille | 1 | 12 | 0 | 1 | 0 |
| Cassiopeia | 0 | 0 | 1 | 0 | 0 |
| Cho’Gath | 0 | 4 | 0 | 10 | 0 |
| Fiora | 0 | 4 | 0 | 0 | 0 |
| Gangplank | 9 | 0 | 0 | 0 | 0 |
| Gnar | 3 | 2 | 0 | 0 | 0 |
| Irelia | 0 | 1 | 0 | 0 | 0 |
| Jax | 0 | 1 | 0 | 0 | 0 |
| Jayce | 1 | 0 | 0 | 0 | 0 |
| Kassadin | 0 | 0 | 1 | 0 | 0 |
| Lucian | 1 | 0 | 0 | 0 | 0 |
| Maokai | 0 | 0 | 0 | 3 | 0 |
| Ornn | 1 | 4 | 0 | 4 | 0 |
| Riven | 1 | 0 | 0 | 0 | 0 |
| Rumble | 0 | 0 | 1 | 0 | 0 |
| Ryze | 0 | 0 | 3 | 0 | 0 |
| Shen | 1 | 7 | 0 | 1 | 0 |
| Singed | 0 | 0 | 0 | 1 | 0 |
| Sion | 0 | 6 | 1 | 0 | 0 |
| Swain | 0 | 0 | 3 | 0 | 0 |
| Trundle | 0 | 4 | 0 | 0 | 0 |
| Vladimir | 0 | 2 | 7 | 1 | 0 |
由上表我们可以看出,上单选手在分类上拥有较低查全率的原因很大部分源于S8春季赛上单英雄的选择的跨度很大还有英雄本身的原因。例如Cassiopeia, Ryze, Swain, Vladimir, Kassadin这类法师,在绝大部分情况下都是典型的中路法师,因此上单选手在选择后也通常会表现得和大多中路选手一样,高AP输出、高吃兵数、高经验等;而Gnar, Gangplank, Jayce同理,本身的英雄属性就非常接近下路的射手,因此数据上表现的相似也是理所因当。Camille, Cho’Gath, Irelia, Jax, Ornn, Shen此类上单英雄我个人则是认为他们本身拥有非常强力的控制或减速,表现得很像S8春季赛出场非常多的控图打野比如Sejuani, Trundle, Skarner等,使得模型误分类。当然,此外也要考虑选手本身在赛场发挥的因素。
通过EDA的数据可视化,我们发现了各个位置的选手在比赛中的数据上似乎是有各自的“特征”的;而我们也通过了KMeans聚类算法,在不提供选手的团队位置这个标签的情况下发现,如果按照团队位置的先验知识(选择K=5),最后的聚类结果是非常符合我们预期的;最后,我们利用KNN分类算法成功地建立了一个根据比赛数据给选手按团队位置分类的分类器,分类的准确率约为86%。